using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
using OpenWaves.Collections;

namespace OpenWaves.Pagination
{
    public static class EnumerableExtensions
    {
        public static IEnumerable<T> GetRange<T>(this IEnumerable<T> collection, int firstItemIndex, int maxItemsCount)
        {
            Contract.Requires<ArgumentNullException>(collection != null);
            Contract.Requires<ArgumentException>(firstItemIndex >= 0);
            Contract.Requires<ArgumentException>(maxItemsCount >= 0);
            Contract.Ensures(Contract.Result<IEnumerable<T>>() != null);

            var lazyCollection = collection as ILazyCollection<T>;
            if (lazyCollection != null)
                return lazyCollection.GetRange(firstItemIndex, maxItemsCount);

            var querableCollection = collection as IQueryable<T>;
            if (querableCollection != null)
                return querableCollection.Skip(firstItemIndex).Take(maxItemsCount);

            return collection.Skip(firstItemIndex).Take(maxItemsCount);
        }

        public static IEnumerable<IPage<T>> Paginate<T>(this IEnumerable<T> items, int pageSize)
        {
            int totalItemsCount;
            return Paginate(items, pageSize, out totalItemsCount);
        }

        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#")]
        public static IEnumerable<IPage<T>> Paginate<T>(this IEnumerable<T> items, int pageSize, out int totalItemsCount)
        {
            Contract.Requires<ArgumentNullException>(items != null);
            Contract.Requires<ArgumentException>(pageSize > 0);
            Contract.Ensures(Contract.Result<IEnumerable<IPage<T>>>() != null);

            var querableCollection = items as IQueryable<T>;
            if (querableCollection != null)
                totalItemsCount = querableCollection.Count();
            else
                totalItemsCount = items.Count();

            return new PageCollection<T>(items, totalItemsCount, pageSize);
        }

        //
        // IList<T> will trigger optimized First(), Last(), Count() paths using indexer and Count property
        //
        private class PageCollection<T> : IList<IPage<T>>, ILazyCollection<IPage<T>>
        {
            private readonly IEnumerable<T> items;
            private readonly int pageSize;
            private readonly IDictionary<int, IPage<T>> pagesLookup = new Dictionary<int, IPage<T>>();
            private readonly int pagesCount;
            private readonly object syncRoot = new object();
            private readonly int totalItemsCount;

            public void CopyTo(Array array, int index)
            {
                foreach (var p in this)
                {
                    array.SetValue(p, index++);
                }
            }

            public int Count
            {
                get { return this.pagesCount; }
            }

            public object SyncRoot
            {
                get { return this.syncRoot; }
            }

            public bool IsSynchronized
            {
                get { return false; }
            }

            public PageCollection(IEnumerable<T> items, int totalItemsCount, int pageSize)
            {
                this.items = items;
                this.totalItemsCount = totalItemsCount;
                this.pagesCount = this.totalItemsCount/pageSize + (this.totalItemsCount%pageSize > 0 ? 1 : 0);
                this.pageSize = pageSize;
            }

            public IPage<T> this[int index]
            {
                get
                {
                    Contract.Requires<ArgumentOutOfRangeException>(index >=0 && index < this.Count);
                    Contract.Ensures(Contract.Result<IPage<T>>() != null);

                    return this.EnsurePage(index);
                }
                set
                {
                    throw new NotSupportedException();
                }
            }

            private IPage<T> EnsurePage(int index)
            {
                return this.pagesLookup.GetOrCreate(index, i => this.CreatePage(index));
            }

            private IPage<T> CreatePage(int index)
            {
                var firstItemIndex = index * this.pageSize;
                var lastItemIndex = Math.Min(this.totalItemsCount, firstItemIndex + this.pageSize) - 1;

                return new Page<T>(this.items, index, firstItemIndex, lastItemIndex);
            }

            public IEnumerable<IPage<T>> GetRange(int firstItemIndex, int maxItemsCount)
            {
                if (firstItemIndex > this.Count - 1)
                    yield break;

                for(int i = 0; i < Math.Min(maxItemsCount, this.Count); i++)
                {
                    yield return this.EnsurePage(firstItemIndex + i);
                }
            }

            public IEnumerator<IPage<T>> GetEnumerator()
            {
                for (int i = 0; i < this.pagesCount; i++)
                {
                    yield return this.EnsurePage(i);
                }
            }

            IEnumerator IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }

            #region IList<T> implementation

            bool ICollection<IPage<T>>.IsReadOnly
            {
                get { return true; }
            }

            void ICollection<IPage<T>>.Add(IPage<T> item)
            {
                throw new NotSupportedException();
            }

            void ICollection<IPage<T>>.Clear()
            {
                throw new NotSupportedException();
            }

            bool ICollection<IPage<T>>.Contains(IPage<T> item)
            {
                throw new NotSupportedException();
            }

            void ICollection<IPage<T>>.CopyTo(IPage<T>[] array, int arrayIndex)
            {                
                var i = arrayIndex;
                foreach (var p in this)
                {
                    array[i++] = p;
                }
            }

            bool ICollection<IPage<T>>.Remove(IPage<T> item)
            {
                throw new NotSupportedException();
            }

            int IList<IPage<T>>.IndexOf(IPage<T> item)
            {
                throw new NotSupportedException();
            }

            void IList<IPage<T>>.Insert(int index, IPage<T> item)
            {
                throw new NotSupportedException();
            }

            void IList<IPage<T>>.RemoveAt(int index)
            {
                throw new NotSupportedException();
            }

            #endregion
        }

        private class Page<T> : IPage<T>
        {
            private readonly int index;
            public int Index
            {
                get { return this.index; }
            }

            private readonly int firstItemIndex;
            public int FirstItemIndex
            {
                get { return this.firstItemIndex; }
            }

            private readonly int lastItemIndex;
            public int LastItemIndex
            {
                get { return this.lastItemIndex; }
            }

            private readonly IEnumerable<T> collection;

            public Page(IEnumerable<T> collection, int index, int firstItemIndex, int lastItemIndex)
            {
                Contract.Requires<ArgumentNullException>(collection != null);
                Contract.Requires<ArgumentOutOfRangeException>(index >= 0);
                Contract.Requires<ArgumentOutOfRangeException>(firstItemIndex >= 0);
                Contract.Requires<ArgumentOutOfRangeException>(lastItemIndex >= firstItemIndex);

                this.collection = collection;
                this.index = index;
                this.firstItemIndex = firstItemIndex;
                this.lastItemIndex = lastItemIndex;
            }

            public IEnumerable<T> GetItemsRange()
            {
                return this.collection.GetRange(this.firstItemIndex, this.lastItemIndex - this.firstItemIndex + 1).ToList();
            }
        }
    }
}